/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.security.shiro.realm;

import com.google.common.collect.Sets;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.permission.AllPermission;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.vacoor.nothing.security.entity.Module;
import org.vacoor.nothing.security.entity.Permission;
import org.vacoor.nothing.security.entity.Role;
import org.vacoor.nothing.security.entity.User;
import org.vacoor.nothing.security.service.PermissionService;
import org.vacoor.nothing.security.service.RoleService;
import org.vacoor.nothing.security.service.UserService;

import java.util.Date;
import java.util.List;
import java.util.Set;

/**
 * DB based AuthorizingRealm
 *
 * @author vacoor
 */
public class ShiroDatabaseRealm extends AuthorizingRealm {
    private static final String RETRY_LIMIT_CACHE_NAME = ShiroDatabaseRealm.class.getName() + ".retryLimitCache";
    private static final long DEFAULT_LOCK_TIME = 10 * 60; // 10 分钟
    private static final int DEFAULT_RETRY_LIMIT = 0;

    private Cache<String/*username*/, Integer> retryLimitCache;
    // private Cache<String/*username*/, Boolean> lockedUserCache;

    private int retryLimit = DEFAULT_RETRY_LIMIT;    /* 重试次数限制 */   // reties
    private long lockTime = DEFAULT_LOCK_TIME;      /* 锁定时间 */

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;

    /**
     * 认证回调
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();

        if (username != null && token.getPassword() != null) {
            User user = userService.findByUsername(username);

            if (user != null) {
                Boolean enabled = user.getEnabled();
                if (enabled == null || !enabled) {
                    throw new DisabledAccountException("user is disabled");
                }

                Boolean locked = user.getLocked();
                if (locked != null && locked) {
                    Long lockedTime = user.getLockedTime();
                    lockedTime = lockedTime != null ? lockedTime : new Date().getTime();

                    // 仍在锁定时间内
                    if (new Date().getTime() - lockedTime < lockTime) {
                        throw new LockedAccountException(String.format("user \"%s\" is locked", username));
                    }

                    // 已经超过锁定时间, 清空锁定信息
                    user.setLockedTime(null);
                    user.setLocked(false);
                    userService.update(user);
                }

                // 密码校验交由 CredentialsMatcher 来处理, 不硬编码加密算法, 返回 authcInfo 后 CredentialsMatcher 会进行比对

                // 注意这里密码是正确的密码
                return new SimpleAuthenticationInfo(username, user.getPassword(), getName());
            }
        }
        throw new UnknownAccountException("user is not exists");
    }

    /**
     * 重写Credential比对, 处理失败次数记录和锁定用户
     */
    @Override
    protected void assertCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        String username = token.getUsername();
        CredentialsMatcher matcher = getCredentialsMatcher();
        boolean isSuper = userService.isSuperAdmin(username);

        if (!isSuper && matcher != null && retryLimit > 0) {
            Cache<String, Integer> cache = getRetryLimitCache();

            if (!matcher.doCredentialsMatch(token, info)) {
                Integer failCount = cache.get(username);
                failCount = failCount != null ? failCount + 1 : 1;

                int remainCount = getRetryLimit() - failCount;
                if (failCount >= retryLimit) {
                    User user = userService.findByUsername(username);
                    user.setLocked(true);
                    user.setLockedTime(new Date().getTime());

                    userService.update(user);
                    cache.remove(username);
                } else {
                    cache.put(username, failCount);
                }
                throw new IncorrectCredentialsException("credential is incorrect, remain count of retries: " + remainCount);
            } else {
                cache.remove(username);
            }
        } else {
            super.assertCredentialsMatch(token, info);
        }
    }

    /**
     * 授权回调
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        String username = (String) getAvailablePrincipal(principals);
        boolean isSuper = userService.isSuperAdmin(username);

        Set<String> roleNames = getRoleNamesForUser(username);
        Set<String> permissions = getPermissions(username, roleNames);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
        if (isSuper) {
            info.addObjectPermission(new AllPermission());
        } else {
            info.addStringPermissions(permissions);
        }
        return info;
    }

    /**
     * 公开暴露
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

    @Override
    protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }

    @Override
    protected void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }

    protected Set<String> getRoleNamesForUser(String username) {
        Set<String> roleNames = Sets.newHashSet();
        List<Role> roles = roleService.findRolesByUsername(username);

        for (Role role : roles) {
            roleNames.add(role.getName());
        }
        return roleNames;
    }

    protected Set<String> getPermissions(String username, Set<String> roleNames) {
        if (roleNames == null) {
            return null;
        }

        Set<String> permissions = Sets.newHashSet();
        for (String roleName : roleNames) {
            List<Permission> perms = permissionService.findPermissionsByRoleName(roleName);
            if (CollectionUtils.isEmpty(perms)) {
                continue;
            }

            for (Permission perm : perms) {
                Module module = perm.getModule();
                String moduleName = module != null ? module.getName() : null;
                String action = perm.getAction();

                if (!StringUtils.hasText(moduleName)) {
                    throw new IllegalStateException("module and module name is must be is not empty: " + perm);
                }

                if (!StringUtils.hasText(action)) {
                    throw new IllegalStateException("action must be is not empty: " + perm);
                }

                permissions.add(moduleName + ":" + action);
            }
        }

        return permissions;
    }

    protected Cache<String/*username*/, Integer> getRetryLimitCache() {
        if (retryLimitCache == null) {
            retryLimitCache = getCacheManager().getCache(RETRY_LIMIT_CACHE_NAME);
        }
        return retryLimitCache;
    }

    /* *********************
     *    getter / setter
     * *********************/

    public int getRetryLimit() {
        return retryLimit;
    }

    public void setRetryLimit(int limit) {
        this.retryLimit = limit;
    }

    public long getLockTime() {
        return lockTime;
    }

    /**
     * 设置锁定时长, 以 ms 为单位
     *
     * @param lockTime
     */
    public void setLockTime(long lockTime) {
        this.lockTime = lockTime;
    }
}
